<?php
/**
 * Анализатор статической структуры страницы
 */
class Analyzer {

    private static $_instance;
    private $http_client;
    /**
     * @var Helper_Dom
     */
    private $domHelper = null;


    public static function getInstance() {
        if ( !self::$_instance ) {
            self::$_instance = new Analyzer();
        }

        return self::$_instance;
    }

    private function __construct() {
        $this->_monthes = array(
            "latin_t1" => "January|Febrary|March|April|May|June|July|August|September|October|November|December",
            "cyrilic_s1" => "(?:март|апрел|ма|июн|июл|август|сентябр|октябр|ноябр|декабр|январ|феврал)(?:ь|я|а|й)"
        );

        $this->_date_patterns = array(
            "^\d+:\d+$",
            "^\d{1,2}\.\d{4,4}$",
            "^\d{4,4}\-\d{1,2}\-\d{1,2}$",
            "^\d{1,2}\.+\d{1,2}\.+\d{1,4}$",
            "^\d{1,4}\.+\d{1,2}\.+\d{1,2}$",
            sprintf("^\d{2,2} %s \d{4,4}$", $this->_monthes['cyrilic_s1']),
            "^\d{1,2}\/\d{1,2}\/\d{1,4}$",
            sprintf("^\d{1,2} %s$", $this->_monthes['cyrilic_s1']),
            "\d{2,2}:\d{2,2}",
            "^\d{1,2}th [A-Za-z]{3,3}, \d{4,4}$",
            sprintf("^(?:%s) \d{1,2}, \d{4,4} \/\/ \d{1,1}:\d{1,2} (?:PM|AM)", $this->_monthes['latin_t1']),
            "^\d{2,2}\.\d{2,2} \d{1,2}:\d{1,2}$",
            "\d{2,2}\.\d{2,2}\.\d{4,4} \d{2,2}:\d{2,2}:\d{2,2}",
            sprintf("^\d{1,2}\s{1,}(%s)+\s{1,}\d{4,4}", $this->_monthes['latin_t1'])
        );

        $this->_semanticMarkupEls = array(
            'tags' => array('strong', 'em', 'big', 'b', 'i', 'u'),
            'css'  => array(
                'font-weight' => 'bold',
                'font-style'  => 'italic',
                'text-decoration' => 'underline',
                'font-size' => '16..20'
            )
        );

        $this->domHelper = new Helper_Dom();
    }

    /**
     * Проверить присутствие некоторых правил CSS
     *
     * @todo Реализовать проверку не только по @style но
     *       и по @class
     *
     * @param DOMElement $node
     * @param Array $rules
     * @return bool
     */
     private function testCSSRules( $node, $rules ) {
        if ( !$this->domHelper->isElementNode($node) || !$node->hasAttribute("style") /**&& !$node->hasAttribute("class")**/ ) {
            return FALSE;
        }

        $style = $node->getAttribute("style");
        foreach ( $rules as $name => $value ) {
            // Получаем значение CSS св-ва
            preg_match( sprintf("/%s:(.?)(?:;|$)/is", $name), $style, $matches );
     
            $prop_value = '';
            if ( isset( $matches[1]) ) {
                $prop_value = $matches[1];
            } else {
                continue;
            }

            $op = '';
            if ( strpos($value, "..") !== FALSE ) {
                $op = "interval";
                $value = explode("..", $value);
            }

            switch ( $op ) {
                case 'interval':
                    return $prop_value > $value[0] && $prop_value < $value[1];
                break;
                default:
                    return $prop_value === $value;
            }
        }

        return FALSE;
    }



    /**
     * Выделено ли содержимое элемент семантической разметкой (полужурное начертание, курсив, т.д.)
     * @param <type> $node
     */
    private function isSemanticSelected($node) {
        foreach( $this->_semanticMarkupEls['tags'] as $el ) {
            if ( $this->domHelper->findChildByName( $node, $el, TRUE ) ) {
                return TRUE;
            }
        }


        return count( $this->testRecursive( $node, array(
                                                        "fn" => array( $this, "testCSSRules"),
                                                        "args" => $this->_semanticMarkupEls['css']
                                                    ))) > 0;

        return FALSE;
    }

    /**
     * Является ли данный узел заголовком новости
     *
     * @param DOMElement $node
     * @return bool
     */
    private function isTitleNode( $node ) {
        $prob = 0;
        if ( $this->domHelper->isElementNode( $node ) ) {
            $prob = $this->isSemanticSelected( $node ) ? 0.2 : 0;
            $prob += $node->nodeName == "a" || $this->domHelper->findChildByName( $node, "a", TRUE ) ? 0.6 : 0;
            $depth = $this->domHelper->getDeepLevel( $node );
            $prob += 0.3/(float)$depth;
        }
        
        $prob += 0.7 / count( explode(" ", trim($node->nodeValue) ) );
        return $prob;
    }

    /**
     * Является ли данный узел описанием новости
     * @param DOMElement $node
     */
    private function isDescriptionNode( $node ) {
        $prob = $this->domHelper->getDeepLevel( $node ) < 2 ? 0.1 : 0;
        $prob += 0.05 * count( explode(" ", trim( $node->nodeValue ) ) );
        $prob += ( strlen( $node->nodeValue ) - strpos( $node->nodeValue, '...' ) ) > 6 ? 0.1 : 0;
        $prob += $this->domHelper->findChildByName( $node, "img", TRUE ) ? 0.1 : 0;

        return $prob;
    }

    /**
     * Является ли данный узел ссылкой на новость
     *
     * @todo Вообще в 65% случаев заголовок служит ссылкой
     *
     * @param <type> $node
     */
    private function isLinkNode( $node ) {
        return $node->nodeName == "a" ? 1 : 0;
    }

    /**
     * Содержит ли данный узел дату
     *
     * @param DOMElement $node
     * @return String
     */
    private function isDate( $node ) {
        $textData = $node->nodeValue;
        foreach ( $this->_date_patterns as $pattern ) {
            if ( preg_match( '/'.$pattern.'/is', $textData ) ) {
                return TRUE;
            }
        }

        return FALSE;
    }

    public function getPage( $url, array $headers = array(), $utf8_convert = TRUE ) {
        $client = $this->getHttpClient();
        $client->setUri( $url );

        if ( !empty($headers) ) {
            $client->setHeaders($headers);
        }

        $response = $client->request();

        if ( $utf8_convert ) {
            $this->convertToUtf($response);
        }

        return $response->getBody();
    }

    protected function getHttpClient() {
        if ( empty( $this->http_client ) ) {
            $client = new Zend_Http_Client();
            $client->setConfig( array(
                'maxredirects'  => 10,
                'timeout'       => 30,
                'useragent'     => 'Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.0.11) Gecko/2009060308 Ubuntu/9.04 (jaunty) Firefox/3.0.11',
                'keepalive'     => TRUE,
            ) );
            $client->setCookieJar();

            $this->http_client = $client;
        }

        return $this->http_client;
    }

    protected function convertToUtf( Zend_Http_Response $response) {
        $headers = $response->getHeaders();
        $text = $response->getBody();
        // Получаем кодировку из тела страницы
        if( preg_match('#<meta\s+http-equiv="Content-Type"\s+content="([\w/]+)(;\s+charset=([^\s"]+))#i', $text, $matches) ) {
            $charset = $matches[3];
            # Удаляем информацию о кодировке чтобы не ругались парсеры
            $text = preg_replace('#<meta\s+http-equiv="Content-Type"\s+content="([\w/]+)(;\s+charset=([^\s"]+))?[^>]*>#i', "", $text);
        } else if( preg_match('#^<\?xml[^>]+encoding=("|\')([^"\']+)("|\')[^>]*\?>#', $text, $matches) ) {
            $charset = $matches[2];
            $text = preg_replace('#^<\?xml[^>]+encoding=("|\')([^"\']+)("|\')[^>]*\?>#', str_replace($charset, 'utf8', $matches[0]), $text);
        } else {
            //Если не удалось - получаем из заголовков
            foreach($headers as $name => $value ) {
                if( $name == 'Content-Type' ) {
                    preg_match( '#([\w/+]+)(;\s+charset=(\S+))?#i', $value, $matches );
                    if ( isset( $matches[3] ) ) {
                        $charset = $matches[3];
                        break;
                    }
                }
            }
        }

        if( isset($charset) && strtolower($charset) != 'utf8' && strtolower($charset) != 'utf-8' ) {
            try {
                $new_text = iconv( $charset, "UTF-8", $text );
            } catch( Exception $e ) {
                _err( 'Не удалось сконвертировать' );
                $new_text = $text; //Не удалось сконвертировать
            }
        }
        return empty( $new_text ) ? $text : $new_text;
    }

    /**
     * Поиск узлов с помощью пользовательской функции
     * 
     * @param DOMElement $node Начало поиска
     * @param String|Array $callback Функция проверки
     * @return array
     */
    private function testRecursive( $node, $callback ) {
        $nodes = array();

        if ( $node->childNodes === NULL || $node->childNodes->length == 0) {
            if ( call_user_func( $callback['fn'], $node, isset($callback['args']) ? $callback['args'] : null  ) ) {
                $nodes[] = $node instanceof DOMText ? $node->parentNode : $node;
            }
        } else {
            for ( $child = $node->firstChild; $child !== NULL; $child = $child->nextSibling ) {
                $nodes = array_merge( $nodes, $this->testRecursive($child, $callback) );
            }
        }

        return $nodes;
    }


    public function fetchUrl( $url ) {
        $site = Doctrine::getTable("Site")->getByUrl( $url );
        
        $result = array();
        try {
            $result = $this->extractItems( $site );
        } catch ( Analyzer_Exception $e ) {
            switch ( $e->getCode() ) {
                case Analyzer_Exception::WRONG_STRUCTURE:
                    $site->markAsWrong( TRUE )
                         ->save();
                break;
            }
        }

        $site->setLastUpdate( time() )
             ->save();

        return $result;
    }


    private function getNodesByProbability( $nodes_list ) {
        $list = array();
        foreach ( $nodes_list as $idx => $node ) {
            $list['link'][$idx] = $this->isLinkNode( $node );
            $list['title'][ $idx ] = $this->isTitleNode($node);
            $list['description'][$idx] = $this->isDescriptionNode($node);
        }

        foreach ( $list as $name => $group ) {
            $list[$name] = $nodes_list[ getMaxId_f($group, $nodes_list) ];
        }

        return $list;
    }


    protected function generateNodeXPath( $container, $node ) {
        $nPathParts = explode( "/", $this->domHelper->buildXPath($node) );
        $cPathParts = explode( "/", $this->domHelper->buildXPath($container) );

        $relNPath = array_slice( $nPathParts, count($cPathParts)+1, count($nPathParts) );
        // Заменяем изменяемую часть на переменную
        $relNPath[0] = preg_replace("/\[([0-9]+?)\]/", "[%d]", $relNPath[0] );

        return join( "/", $relNPath );
    }

    public function retreiveStructure( Site $site, $dom ) {
        $date_nodes = $this->testRecursive( $dom->getElementsByTagName("body")->item(0), array(
                                                                                        "fn" => array($this, "isDate")
                                                                                    ));
        Zend_Debug::dd($date_nodes);
        $containerNode = $this->domHelper->getNearSameParent( $date_nodes );
        if ( count($date_nodes) < 1 || !$this->domHelper->isParent($containerNode, $date_nodes[0], TRUE ) ) {
            throw new Analyzer_Exception( Analyzer_Exception::WRONG_STRUCTURE );
        }

        $site_structure = new Data_Site_Structure();
        $site_structure->setContainerNode( $containerNode )
                       ->setDateNodes( $date_nodes )
                       ->setUrl( $site->getUrl() );

        return $site_structure;
    }

    public function fetchNode( array $node_parts, $stream_url ) {
        $node = new Data_Node();
        
        if ( isset($node_parts['description']) )
            $node->setDescription( $node_parts['description'] );

        if ( isset($node_parts['title']) )
            $node->setTitle( $node_parts['title'] );

        if ( isset($node_parts['link']) )
            $node->setLink( $node_parts['link'] );

        $node->setStreamUrl( $stream_url );

        if ( $node->getTitle() !== FALSE && $node->getDate() !== FALSE  ) {
            return $node;
        }

        return FALSE;
    }

    /**
     * Получить карту узлов для данного сайта в виде массива XPath-путей.
     *
     * @example
     * Analyzer::getInstance()->getNodesMap(....);
     * => Result:
     *  array(
     *      "item" => ""#document/html/body/table/...../td{i}"
     *      "title" => "#document/html/body/table/...../td[2]/span[0]",
     *      "description" => "#document/html/body/table/...../td[3]/span[{i+1}]",
     *      "link" => ""#document/html/body/table/...../td[2]/span[0]/a"
     *  )
     * @param String $data
     */
    protected function extractItems( $site ) {
        $items = array();
        $data = $this->getPage( $site->getUrl() );
        $dom = $this->domHelper->getDOMDoc( $data );
        
        $struct = $this->retreiveStructure($site, $dom);
        
        $date_nodes = $struct->getDateNodes();
        foreach( $date_nodes as $idx => $node ) {
            if ( !isset( $date_nodes[$idx+1] ) ) {
                break;
            }
            // Мы нашли общий root-элемент только для "большинства" элементов
            // поэтому те, для которых он не родитель, мы попросту отбрасываем
            if ( !$this->domHelper->isParent($struct->getContainerNode(), $node, TRUE) ) {
                continue;
            }

            $between = $this->domHelper->getElementsBetween ( $node, $date_nodes[$idx+1], $struct->getContainerNode() );
            $nodes = $this->getNodesByProbability( $between );

            if ( FALSE !== ( $item = $this->fetchNode($nodes, $struct->getUrl() ) ) ){
                $items[] = $item;
            }

        }

        return $items;
    }

}